![]() |
![]() |
|
Die Methode BeginInvoke ist sehr mächtig, denn mit ihrem Aufruf auf die Referenz eines Delegaten wird ein Hintergrundthread erzeugt, in dem die vom Delegaten beschriebene Methode ausgeführt wird. Der aufrufende Thread macht mit seiner eigenen Arbeit weiter, anstatt auf die Beendigung der aufgerufenen Methode zu warten. Dazu ein kleines Beispiel. Nehmen wir an, dass die Methode AsyncTestProc, die eine längere Zeit zur Ausführung benötigt, aufgerufen werden soll. AsyncTestProc sei wie folgt definiert:
Ein Client, der diese Methode asynchron ausführen möchte, kann einen Delegaten deklarieren und diesem die Adresse der Methode AsyncTestProc übergeben:
Das reicht bereits aus, um AsyncTestProc in einem separaten Thread abzuarbeiten. Dem Aufruf von BeginInvoke müssen Argumente übergeben werden, die unsere Anweisung noch nicht enthält. Sehen wir uns deshalb nun die Definition von BeginInvoke an.
Aufgerufen wird BeginInvoke auf die Instanz eines Delegaten, der auf eine bestimmte Methode zeigt. Weist die aufzurufende Methode eine Parameterliste auf, müssen die erforderlichen Argumente von BeginInvoke an die Methode weitergeleitet werden. Dazu dient die optionale Parameterliste. Theoretisch wäre das bereits vollkommen ausreichend, um die aufgerufene Methode asynchron auszuführen. Meistens benötigt der Aufrufer aber Kenntnis von der Beendigung der asynchronen Ausführung, beispielsweise wenn er die Rückgabewerte verarbeitet. Folglich muss es eine Möglichkeit geben, die es der asynchron aufgerufenen Methode ermöglicht, den Aufrufer davon zu unterrichten, dass sie ihre Operationen beendet hat. Dabei kann es sich nur um den Aufruf einer Methode im Initiator der asynchronen Operation handeln. Konsequenterweise muss der asynchron aufgerufenen Methode die Adresse der Rückrufmethode im Aufrufer bekannt ist. Das klingt wieder verdächtig nach einem Delegaten – und tatsächlich ist dem so, denn dem Aufruf von BeginInvoke werden nicht nur die Argumente übergeben, welche die asynchron aufgerufene Methode benötigt, sondern darüber hinaus auch ein Objekt vom Typ AsyncCallback, bei dem es sich um den erforderlichen Delegaten handelt. Die Definition des Delegaten AsyncCallback lautet:
Die Methode, die aus der asynchron ausgeführten zurückgerufen wird, muss den Rückgabetyp void aufweisen und einen Parameter vom Typ IAsyncResult definieren. BeginInvoke verfügt noch über einen weiteren Parameter vom Typ object. Hier kann beim Start der asynchronen Operation ein beliebiges Objekt übergeben werden, das Informationen beliebiger Art enthält. Das hört sich komplizierter an, als es tatsächlich ist. Daher wollen wir den Ablauf schrittweise an einem kleinen Beispiel verfolgen. Gegeben seien dazu die beiden Klassen Program und ClassA wie folgt:
Aus Main heraus soll die Methode AsyncTest in der Klasse ClassA asynchron aufgerufen werden. Diese Forderung bewirkt, dass wir BeginInvoke auf einen Delegaten aufrufen müssen, der die asynchron auszuführende Methode im Objekt vom Typ ClassA beschreibt. Dazu wird zunächst auf Klassenebene der Klasse Program ein Delegat mit
deklariert. Anschließend verschaffen wir uns ein Objekt vom Typ des Delegaten, dem als Argument die asynchron auszuführende Methode übergeben wird.
Mit
wird die asynchrone Ausführung von AsyncTest in einem Hintergrundthread gestartet. Allerdings ist die Anweisung noch unvollständig – symbolisiert durch die Punkte. Wir sollten in Program nämlich noch eine Methode bereitstellen, über die der Hintergrundthread das Objekt vom Typ Program über das Ende seiner Operation benachrichtigt. Die Definition der Rückrufmethode muss der des Delegaten AsyncCallback entsprechen, demnach also einen Parameter vom Typ IAsyncResult enthalten. Wir nennen diese Methode MyCallBackProc.
Das Objekt vom Typ IAsyncResult entspricht dem Rückgabewert von BeginInvoke. Es veröffentlicht insgesamt sechs Eigenschaften. Dazu gehört unter anderem auch IsCompleted. Über IsCompleted kann der Aufrufer jederzeit feststellen, ob die asynchrone Ausführung bereits beendet ist. Eine zweite, sehr interessante Eigenschaft ist AsyncState, die genau das Objekt abruft, das als letzter Parameter dem Aufruf von BeginInvoke übergeben worden ist. Sie werden später in einem anderen Beispiel die sinnvolle Auswertung dieses Objekts sehen. Wir wollen nun unser Beispiel komplettieren und sowohl innerhalb des Servers als auch innerhalb des Clients Code einsetzen, der tatsächlich einige Zeit in Anspruch nimmt, damit wir den Effekt des asynchronen Aufrufs tatsächlich beobachten können.
In der Abbildung 11.11 ist das Ergebnis des Aufrufs zu sehen. Es ist eindeutig zu erkennen, dass .P. bzw. .X. mehr oder weniger abwechselnd ausgegeben werden, denn beide Methoden arbeiten parallel. Beendet wird die asynchrone Operation durch den Rückruf von MyCallbackProc, was durch die Ausgabe des bekannten Satzes »Ich habe fertig« bestätigt wird.
Abbildung 11.11 Ausgabe eines asynchronen Aufrufs Beispielprogramm – Pumpen asynchron einschaltenDas Beispielprogramm der Pumpenschaltung aus Kapitel 7 zeigte, wie die Pumpen mittels Rückruf den Client über das Einschalten der Motoren in Kenntnis setzten. In diesem Beispiel wurden die Pumpen der Reihe nach eingeschaltet. Dabei wurde jedoch nicht berücksichtigt, dass der Startvorgang durchaus eine gewisse Zeit in Anspruch nehmen kann. Jetzt soll das Programm so abgeändert werden, dass die Einschaltvorgänge nicht mehr hintereinander, sondern gleichzeitig erfolgen.
In diesem Beispielprogramm werden nicht die Delegaten auf die Startmethoden an die steuernde Klasse übergeben, sondern die Referenzen auf die Pumpen. Damit wir später die Pumpen identifizieren können, die sich bei der Rückrufmethode nach dem erfolgreichen Starten melden, ist die Pumpenklasse um einen Konstruktor und die Eigenschaft Bezeichner ergänzt worden. Die Eigenschaft liefert den Inhalt des Feldes bezeichner zurück. Weil alle Pumpen dieselbe Methode im Client zurückrufen, bietet es sich an, der Steuerklasse den Delegaten auf diese Methode zu übergeben:
Die Referenz auf den übergebenen Delegaten hält das Objekt der Klasse ControlPumps in einer privaten Variablen vor. Nachdem alle Pumpen in der Steuerklasse registriert sind, können sie durch den Aufruf von StartAllPumps im Client gestartet werden. In einer foreach-Schleife werden alle in der internen Auflistung gespeicherten Pumpenobjekte durchlaufen und für jedes Objekt ein Delegat erzeugt, der auf die objektspezifische Startmethode zeigt. Mit
wird anschließend BeginInvoke auf diesem Delegaten aufgerufen. Damit gewährleisten wir, dass die Startmethode der jeweiligen Pumpe in einem eigenen Thread ausgeführt wird. Das Besondere an diesem Aufruf ist, dass wir dem zweiten Parameter ein Zustandsobjekt übergeben – es handelt es dabei um die Zeichenfolge, die eine Pumpe eindeutig beschreibt. In der Rückrufmethode des Clients kann das dem BeginInvoke-Aufruf übergebene Zustandsobjekt, das von der Steuerklasse an den Thread weitergereicht und von der Rückrufmethode übergeben wird, ausgewertet werden. Dazu dient die Eigenschaft AsyncState des IAsyncResult-Parameters. Damit wissen wir nicht nur, dass eine Pumpe angelaufen ist, sondern gleichzeitig auch, um welche es sich handelt.
11.4.2 Asynchroner Aufruf mit Rückgabewerten
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public Datentyp EndInvoke([Parameterliste,] IAsyncResult); |
Wie bei BeginInvoke müssen Sie auch EndInvoke eine vorgeschriebenen Parameterliste übergeben, die nicht identisch mit der Parameterliste von BeginInvoke ist: Sie darf nur die Referenzparameter der asynchronen Methode enthalten, damit diese ihre Resultate dort hineinschreiben kann. Die Angabe der Werteparameter ist nicht erlaubt. Der einzige grundsätzlich immer zwingend erforderliche Parameter ist vom Typ IAsyncResult. Hier wird das Objekt übergeben, das die Rückrufmethode des Clients vom Server erhalten hat.
Wir wollen nun das Beispiel AsynchronerAufruf_1 ändern, um zu sehen, wie eine asynchrone Methode behandelt wird, die sowohl Werte- als auch Referenzparameter erwartet und darüber hinaus auch noch einen Rückgabewert hat. Dazu implementieren wir die Methode AsyncTest wie folgt:
| public string AsyncTest(int x, ref long y) { |
| // zeitaufwändige Ausführung |
| for(int i = 0; i <= 30; i++) { |
| Console.Write(".X."); |
| Thread.Sleep(10); |
| } |
| y = 12345; |
| return "Ich habe fertig."; |
| } |
Die Parameterliste enthält jetzt den Referenzparameter y und den Werteparameter x, außerdem liefert die Methode eine Zeichenfolge zurück.
Die Änderung der Signatur hat natürlich auch im auslösenden Thread Konsequenzen. Der Delegat, der den Aufruf der Methode kapselt, muss an die veränderten Bedingungen angepasst werden:
| public delegate string MyDelegate(int x, ref long y); |
Gleiches gilt auch für den Start der asynchronen Bearbeitung, denn nun reicht es nicht mehr aus, mit BeginInvoke einfach nur einen Delegaten auf die Rückrufmethode zu übergeben sowie die Referenz auf ein Objekt, in das der asynchrone Aufruf Informationen schreiben könnte. Wir müssen stattdessen auch die Parameter der asynchronen Methode in der richtigen Reihenfolge bedienen:
| del.BeginInvoke(intVar, ref lngVar, callback, null); |
AsyncTest nimmt nun eine Kopie des int-Wertes und die Adresse des long-Wertes entgegen, kann mit diesen die erforderlichen Operationen ausführen und zum Abschluss durch Aufruf der über callback bekannt gegebenen Adresse die Methode MyCallbackProc informieren.
Der Implementierung der Rückrufmethode kommt nun eine entscheidende Bedeutung zu. Es gilt, sowohl den Rückgabewert als auch den in diesem Fall geänderten Inhalt der Variablen lngVar auszuwerten. Dem Aufruf von EndInvoke übergeben wir die Adresse von lngVar und holen uns den Rückgabewert an der Konsole ab:
| public static void MyCallbackProc(IAsyncResult ar) { |
| // zeigt den Rückgabewert der asynchronen Methode an |
| Console.Write(del.EndInvoke(ref lngVar, ar)); |
| // schreibt den Inhalt des Referenzparameters lngVar |
| Console.Write("..Wert y = {0}", lngVar); |
| } |
Die Konsolenausgabe bestätigt, dass unser Unterfangen von Erfolg beschieden ist: Wir erhalten sowohl die Zeichenfolge als auch den veränderten Inhalt des Feldes intVar.
Zum Abschluss fassen wir das Beispielprogramm noch einmal zusammen.
| // -------------------------------------------------------------- |
| // Beispiel: ...\ Kapitel 11\AsynchronerAufruf_2 |
| // -------------------------------------------------------------- |
| class Program { |
| // --------- G e ä n d e r t -------------- |
| public delegate string MyDelegate(int x, ref long y); |
| // -----------E r g ä n z t --------------- |
| private static MyDelegate del; |
| private static int intVar = 4711; |
| private static long lngVar; |
| static void Main(string[] args) { |
| ClassA obj = new ClassA(); |
| // Delegat, der die asynchron aufzurufende Methode beschreibt |
| del = new MyDelegate(obj.AsyncTest); |
| // Delegate vom Typ AsyncCallback beschreibt die Methode, die nach |
| // Beendigung der asynchronen Ausführung aufgerufen wird |
| AsyncCallback callback = new AsyncCallback(MyCallbackProc); |
| // die Methode AsyncTest in ClassA asynchron aufrufen |
| // --------- G e ä n d e r t -------------- |
| del.BeginInvoke(intVar, ref lngVar, callback,null); |
| // zeitaufwändige Ausführung |
| for(int i = 0; i <= 100; i++) { |
| Console.Write(".P."); |
| Thread.Sleep(10); |
| } |
| Console.ReadLine(); |
| } |
| public static void MyCallbackProc(IAsyncResult ar) { |
| // --------- G e ä n d e r t -------------- |
| // zeigt den Rückgabewert der asynchronen Methode an |
| Console.Write(del.EndInvoke(ref lngVar, ar)); |
| // schreibt den Inhalt des Referenzparameters lngVar |
| Console.Write("..Wert y = {0}", lngVar); |
| } |
| } |
| class ClassA { |
| // --------- G e ä n d e r t -------------- |
| public string AsyncTest(int x, ref long y) { |
| // zeitaufwändige Ausführung |
| for(int i = 0; i <= 30; i++) { |
| Console.Write(".X."); |
| Thread.Sleep(10); |
| } |
| y = 12345; |
| return "Ich habe fertig."; |
| } |
| } |
Interessant ist, innerhalb der Rückrufmethode ein wenig zu spielen. Wenn Sie beispielsweise der Meinung sind, Sie könnten auch mit
| // funktioniert nicht!!! |
| public static void MyCallbackProc(IAsyncResult ar) { |
| Console.Write(ref lngVar); |
| } |
den Inhalt der als Referenz übergebenen Variablen lngVar auswerten, werden Sie Schiffbruch erleiden. Zu keinem Zeitpunkt schreibt die Methode des Hintergrundthreads den Inhalt 12345 in das Original. Ohne den Aufruf von EndInvoke bleibt das Original unbeeinflusst – es fehlt der äußere Anstoß, die neuen Daten zu übernehmen. Erst mit EndInvoke stehen diese zur Verfügung.
Wir könnten auch auf die Idee kommen, eine neue Variable vom Typ des Interface IAsyncResult zu deklarieren und diese an EndInvoke weiterreichen anstelle der im Parameter der Rückrufmethode entgegengenommenen:
| // funktioniert auch nicht!!! |
| public void MyCallbackProc(IAsyncResult ar) { |
| IAsyncResult newAsyncResult; |
| ... |
| Console.Write(del.EndInvoke(ref lngVar, newAsyncResult)); |
| Console.Write(lngVar); |
| } |
Dieser Aufruf wird wirkungslos bleiben und zur Laufzeit im Nirwana enden. Denn was die Rückrufmethode MyCallbackProc im Parameter ar entgegennimmt, dient ihr gleichzeitig zum Aufruf eines ganz bestimmten Objekts des Servers – der IASyncResult-Parameter schafft die Möglichkeit der wechselseitigen Kommunikation. Wir haben es hier gewissermaßen mit einer Hintereinanderschaltung von Rückrufen zu tun.
Am Anfang dieses Abschnitts wurde schon darauf hingewiesen, dass einige Klassen der .NET-Klassenbibliothek Methoden mit asynchroner Verarbeitung anbieten. Die Klasse FileStream im Namespace System.IO ist ein Beispiel dafür. Es werden allerdings nicht die Methoden BeginInvoke und EndInvoke aufgerufen, sondern zwei ähnlich lautende: BeginRead und EndRead bzw. BeginWrite und EndWrite.
Wir wollen uns nun ansehen, wie eine Klasse aufgebaut ist, die ähnlich wie FileStream implementiert ist. Dabei lernen wir einerseits, wie wir die asynchronen Methoden der Klassen des .NET Frameworks behandeln müssen, andererseits aber auch, diese Technik in eigenen Klassen zu nutzen.
Am Anfang steht die Idee, eine Methode zu entwickeln, von der wir annehmen, dass sie in Abhängigkeit von den Umgebungsbedingungen und der Art der Operation eine längere Zeit zur Bearbeitung in Anspruch nehmen kann. Wir wollen diese Methode nachfolgend Calculate nennen, die Klasse dazu Server.
| class Server { |
| public int Calculate(int x) { |
| Console.Write("---Bearbeitung startet---"); |
| for (int i = 0; i <= 20; i++) { |
| Console.Write(".X."); |
| Thread.Sleep(10); |
| } |
| Console.Write("---Bearbeitung beendet---"); |
| return x * x; |
| } |
| } |
Die for-Schleife simuliert eine länger andauernde Operation. Diese Implementierung arbeitet synchron. Da wir uns bewusst sind, dass Calculate vielleicht auch eine Stunde zur vollständigen Ausführung brauchen könnte (wir sind mit unserer Annahme sehr großzügig), bieten wir zusätzlich eine asynchrone Variante an. Dazu benötigen wir zwei weitere Methoden, die einer allgemeinen Konvention folgend als BeginXxx und EndXxx bezeichnet werden – in unserer Klasse demnach BeginCalculate und EndCalculate. Die noch unvollständige Klassenstruktur sieht dann folgendermaßen aus:
| class Server { |
| // Methode Calculate wird synchron ausgeführt |
| public int Calculate(int x) { |
| // Anweisungen |
| } |
| // Start der asynchronen Ausführung |
| public ... BeginCalculate(...) { |
| // Anweisungen, u. a. der Aufruf von Calculate |
| } |
| // Beenden der asynchronen Ausführung |
| public ... EndCalculate(.) { |
| // Anweisungen |
| } |
| } |
An dieser Stelle kommt es zu der wichtigsten Entscheidung überhaupt. Was wir beabsichtigen, ist die asynchrone Ausführung der Methode Calculate. Asynchronität heißt aber auch, dass ein weiterer Thread gestartet werden muss, sobald die Methode BeginCalculate aufgerufen wird. Wenn wir in dieser Methode ein Objekt vom Typ Thread erzeugen und seinem Konstruktor einen Delegaten übergeben, bräuchten wir auch noch ein Objekt, welches das Interface IAsyncResult implementiert, müssten zwangsläufig dessen Methoden implementieren usw.
Die Entwicklung auf diese Weise zu gestalten, ist sehr aufwändig. Es gibt eine viel einfachere Lösung, da die beiden Methoden BeginInvoke und EndInvoke genau das leisten, was wir brauchen. Also benutzen wir sie auch, um das Ziel effizient zu erreichen. Dazu wird die Logik, die in den Abschnitten 11.4.2 und 11.4.3 beschrieben wurde, innerhalb der Klasse Server implementiert.
| // -------------------------------------------------------------- |
| // Beispiel: ...\ Kapitel 11\AsynchronerAufruf_3 |
| // -------------------------------------------------------------- |
| class Server { |
| // Deklaration eines Delegaten, der den Funktionsaufruf |
| // von 'Caluculate' beschreibt |
| public delegate int CalculateHandler(int x); |
| CalculateHandler del; |
| // Methode Calculate wird synchron ausgeführt |
| public int Calculate(int x) { |
| Console.Write("---Bearbeitung startet---"); |
| for (int i = 0; i <= 20; i++) { |
| Console.Write(".X."); |
| Thread.Sleep(10); |
| } |
| Console.Write("---Bearbeitung beendet---"); |
| return x * x; |
| } |
| // Start der asynchronen Ausführung |
| public IAsyncResult BeginCalculate(int intVar, |
| AsyncCallback asyncCallback, object state) { |
| del = new CalculateHandler(Calculate); |
| // Aufruf der Methode Calculate, die in einem eigenen |
| // Thread ausgeführt wird |
| return del.BeginInvoke(intVar, asyncCallback, null); |
| } |
| // Beenden der asynchronen Ausführung |
| public int EndCalculate(IAsyncResult asyncResult) { |
| return del.EndInvoke(asyncResult); |
| } |
| } |
Dem Aufruf der Methode BeginCalculate werden die Daten übergeben, welche die Methode Calculate für ihre Operation benötigt. In unserem Beispiel handelt es sich nur um einen als Werteparameter deklarierten Integer. Der zweite Parameter erhält die Referenz auf einen Delegaten, der die Rückrufmethode im Aufrufer beschreibt. Der dritte und letzte Parameter dient dazu, ein Objekt bereitzustellen, mit dem Daten zwischen dem aufrufenden und dem aufgerufenen Objekt ausgetauscht werden. Ein solches Objekt ist in unserem Beispielcode nicht vorgesehen.
Der Aufruf von BeginCalculate orientiert sich an dem von BeginInvoke – und das ist typisch für Klassen im .NET Framework, die asynchrone Methoden offen legen. Unter ähnlicher Prämisse wird auch EndCalculate implementiert, der Rückgabewert des internen EndInvoke-Aufrufs wird zum Rückgabewert der Instanzmethode.
Es bleibt zum Schluss noch zu testen, ob die Klassenimplementierung auch unseren Anforderungen genügt. Dazu entwickeln wir einen Client mit der Methode Start zum Aufruf der asynchronen Ausführung und einer Methode Results, die als Rückruffunktion vom Server angesteuert wird.
| class Client { |
| private Server myObj = new Server(); |
| static void Main(string[] args) { |
| Client myClient = new Client(); |
| myClient.Start(); |
| } |
| public void Start() { |
| int iVar = 23; |
| AsyncCallback callback = new AsyncCallback(this.Results); |
| // Aufruf der asynchronen Ausführung |
| IAsyncResult ia = myObj.BeginCalculate(iVar, callback, null); |
| for (int i = 0; i <= 100; i++) { |
| Console.Write(".{0}.", i); |
| Thread.Sleep(5); |
| } |
| Console.ReadLine(); |
| } |
| // diese Methode wird vom Server aufgerufen |
| public void Results(IAsyncResult asyncResult) { |
| // das Ergebnis der asynchronen Operation abholen |
| int res = myObj.EndCalculate(asyncResult); |
| Console.Write("---Resultat = {0} ", res); |
| Console.Write("---FERTIG---"); |
| } |
| } |
Die Ausgabe an der Konsole wird wie in der folgenden Abbildung gezeigt aussehen.

Hier klicken, um das Bild zu vergrößern
Abbildung 11.12 Ausgabe des Beispiels »AsynchronerAufruf_3«
| Hinweis Sie sollten grundsätzlich immer, wenn die Methode einer Klasse eine länger andauernde Operation ausführt, neben der asynchronen Variante auch die synchron arbeitende Methode anbieten, um dem Benutzer die Entscheidung zu überlassen, ob er die synchrone oder asynchrone Variante aufrufen möchte. |
| << zurück |
|
||||||||||||||
|
||||||||||||||
|
||||||||||||||
|
||||||||||||||
Copyright © Galileo Press 2006
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das <openbook> denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.